package org.fcrepo.server.validation.ecm;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.fcrepo.server.Context;
import org.fcrepo.server.errors.ServerException;
import org.fcrepo.server.errors.StreamIOException;
import org.fcrepo.server.storage.ContentManagerParams;
import org.fcrepo.server.storage.DOReader;
import org.fcrepo.server.storage.ExternalContentManager;
import org.fcrepo.server.storage.types.Datastream;
import org.fcrepo.server.storage.types.MIMETypedStream;
import org.fcrepo.server.storage.types.Validation;
import org.fcrepo.server.validation.ecm.jaxb.DsTypeModel;
import org.fcrepo.server.validation.ecm.jaxb.Extension;
import org.w3c.dom.Element;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* Created by IntelliJ IDEA.
* User: abr
* Date: Jun 24, 2010
* Time: 10:11:55 AM
* To change this template use File | Settings | File Templates.
*/
public class SchemaValidator {
public SchemaValidator() {
}
void validate(Context context, DsTypeModel typeModel, Datastream objectDatastream, Validation validation,
DOReader contentmodelReader, Date asOfDateTime, ExternalContentManager m_exExternalContentManager) throws ServerException {
List<Extension> extensions = typeModel.getExtension();
List<List<String>> schemaStreamsToProblemsMap = new ArrayList<List<String>>();
for (Extension extension : extensions) {
String name = extension.getName();
Element reference = null;
Source source = null;
if (!"SCHEMA".equals(name)) {//ignore non schema extensions
continue;
}
List<Element> contents = extension.getAny();
for (Element content : contents) { //find the reference
String tagname = content.getTagName();
if (tagname.equals("reference")) {
reference = content;
break;
}
}
if (reference == null){ //if no reference
boolean found = false;
for (Element content : contents) {
if ( content.getNodeType() == Element.ELEMENT_NODE){
source = new DOMSource(content);//parse the inline schema
found = true;
break;
}
}
if (!found){//empty tag
List<String> validationProblems = validation.getDatastreamProblems(objectDatastream.DatastreamID);
validationProblems.add(Errors.schemaNotFound(contentmodelReader.GetObjectPID()));
validation.setValid(false);
}
}
else {
String type = reference.getAttribute("type");
String value = reference.getAttribute("value");
if ("datastream".equalsIgnoreCase(type)) {
Datastream schemaDS = contentmodelReader.GetDatastream(value, asOfDateTime);
if (schemaDS == null) {//No schema datastream, ignore and continue
continue;
}
InputStream schemaStream;
schemaStream = schemaDS.getContentStream();
source = new StreamSource(schemaStream);
} else if ("url".equalsIgnoreCase(type)){
InputStream schemaStream;
ContentManagerParams params = new ContentManagerParams(value);
MIMETypedStream externalContent = m_exExternalContentManager.getExternalContent(params);
schemaStream = externalContent.getStream();
source = new StreamSource(schemaStream);
} else { //reference used, but type not recognized
List<String> validationProblems = validation.getDatastreamProblems(objectDatastream.DatastreamID);
validationProblems.add(Errors.schemaNotFound(contentmodelReader.GetObjectPID()));
validation.setValid(false);
continue;
}
}
LSResourceResolver resourceResolver
= new ResourceResolver(contentmodelReader, asOfDateTime);
Schema schema;
try {
schema = parseAsSchema(source, resourceResolver);
} catch (SAXException e) {
List<String> validationProblems = validation.getDatastreamProblems(objectDatastream.DatastreamID);
validationProblems.add(Errors.schemaCannotParse(contentmodelReader.GetObjectPID(),objectDatastream.DatastreamID,e));
validation.setValid(false);
continue;
}
List<String> problems = checkSchema( objectDatastream.getContentStream(),
schema,
contentmodelReader.GetObjectPID(),
objectDatastream.DatastreamID);
schemaStreamsToProblemsMap.add(problems);
if (problems.isEmpty()) {//if multiple SCHEMAS have been defined, only one is needed to be compliant. If one schema
//produce no errors, do not bother validating against the others.
break;
}
}
boolean foundProblem = false;
for (List<String> problems : schemaStreamsToProblemsMap) {
if (!problems.isEmpty()){
foundProblem = true;
break;
}
}
if (foundProblem) {
validation.setValid(false);
List<String> validationProblems = validation.getDatastreamProblems(objectDatastream.DatastreamID);
for (List<String> problems : schemaStreamsToProblemsMap) {
validationProblems.addAll(problems);
}
}
}
private Schema parseAsSchema(Source input, LSResourceResolver resolver) throws SAXException {
SchemaFactory schemaFactory = SchemaFactory
.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema;
schemaFactory.setResourceResolver(resolver);
schema = schemaFactory.newSchema(input);
return schema;
}
public List<String> checkSchema(InputStream objectStream, Schema schema,
String contentModel,
String datastreamID) {
List<String> problems = new ArrayList<String>();
ErrorHandler errorhandler = new ReportingErrorHandler(problems, contentModel, datastreamID);
try {
Validator validator = schema.newValidator();
validator.setErrorHandler(errorhandler);
validator.validate(new StreamSource(objectStream));
} catch (SAXException e) {
problems.add(Errors.invalidContentInDatastream(datastreamID,contentModel,e));
} catch (IOException e) {
problems.add(Errors.unableToReadDatastream(datastreamID,e));
}
return problems;
}
public static class ResourceResolver implements LSResourceResolver {
private DOReader contentmodelReader;
private Date asOfDateTime;
public ResourceResolver(DOReader contentmodelReader, Date asOfDateTime) {
//To change body of created methods use File | Settings | File Templates.
this.contentmodelReader = contentmodelReader;
this.asOfDateTime = asOfDateTime;
}
@Override
public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId,
String baseURI) {
//The key here is SystemID.
if (systemId == null) {
return null;
}
if (systemId.startsWith("$THIS$/")) {//other datastream in this object
String[] tokens = systemId.split("/");
//String is of the format $THIS$/DSID
try {
final Datastream schemastream = contentmodelReader.GetDatastream(tokens[1], asOfDateTime);
LSInput input = new MyLSInput(schemastream);
input.setBaseURI(baseURI);
input.setPublicId(publicId);
input.setBaseURI(baseURI);
return input;
} catch (ServerException e) {
return null;
}
} else {
return null;
}
}
public static class MyLSInput implements LSInput {
Datastream stream;
public MyLSInput(Datastream stream) {
this.stream = stream;
}
private String systemId, publicId, baseURI;
@Override
public Reader getCharacterStream() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void setCharacterStream(Reader characterStream) {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public InputStream getByteStream() {
try {
return stream.getContentStream();
} catch (StreamIOException e) {
return null;
}
}
@Override
public void setByteStream(InputStream byteStream) {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public String getStringData() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void setStringData(String stringData) {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public String getSystemId() {
return systemId;
}
@Override
public void setSystemId(String systemId) {
this.systemId = systemId;
}
@Override
public String getPublicId() {
return publicId;
}
@Override
public void setPublicId(String publicId) {
this.publicId = publicId;
}
@Override
public String getBaseURI() {
return baseURI;
}
@Override
public void setBaseURI(String baseURI) {
this.baseURI = baseURI;
}
@Override
public String getEncoding() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void setEncoding(String encoding) {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public boolean getCertifiedText() {
return false; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void setCertifiedText(boolean certifiedText) {
//To change body of implemented methods use File | Settings | File Templates.
}
}
;
}
/**
* Errorhandler used by ECM to get errors reported correctly.
*/
public static class ReportingErrorHandler implements ErrorHandler {
private List<String> problems;
private String contentModel;
private String datastreamID;
public ReportingErrorHandler(List<String> problems,
String contentModel,
String datastreamID) {
this.problems = problems;
this.contentModel = contentModel;
this.datastreamID = datastreamID;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
//TODO should these be reported?
problems.add(Errors.schemaValidationWarning(datastreamID,contentModel,exception));
}
@Override
public void error(SAXParseException exception) throws SAXException {
problems.add(Errors.schemaValidationError(datastreamID,contentModel,exception));
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
problems.add(Errors.schemaValidationFatalError(datastreamID,contentModel,exception));
}
}
}